// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/frame_host/frame_tree_node_blame_context.h"

#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/test/trace_event_analyzer.h"
#include "base/trace_event/trace_buffer.h"
#include "base/trace_event/trace_event_argument.h"
#include "content/browser/frame_host/frame_tree.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/common/frame_owner_properties.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/web/WebSandboxFlags.h"

namespace content {

namespace {

    bool EventPointerCompare(const trace_analyzer::TraceEvent* lhs,
        const trace_analyzer::TraceEvent* rhs)
    {
        CHECK(lhs);
        CHECK(rhs);
        return *lhs < *rhs;
    }

    void OnTraceDataCollected(base::Closure quit_closure,
        base::trace_event::TraceResultBuffer* buffer,
        const scoped_refptr<base::RefCountedString>& json,
        bool has_more_events)
    {
        buffer->AddFragment(json->data());
        if (!has_more_events)
            quit_closure.Run();
    }

    void ExpectFrameTreeNodeObject(const trace_analyzer::TraceEvent* event)
    {
        EXPECT_EQ("navigation", event->category);
        EXPECT_EQ("FrameTreeNode", event->name);
    }

    void ExpectFrameTreeNodeSnapshot(const trace_analyzer::TraceEvent* event)
    {
        ExpectFrameTreeNodeObject(event);
        EXPECT_TRUE(event->HasArg("snapshot"));
        EXPECT_TRUE(event->arg_values.at("snapshot")
                        ->IsType(base::Value::Type::DICTIONARY));
    }

    std::string GetParentNodeID(const trace_analyzer::TraceEvent* event)
    {
        const base::Value* arg_snapshot = event->arg_values.at("snapshot").get();
        const base::DictionaryValue* snapshot;
        EXPECT_TRUE(arg_snapshot->GetAsDictionary(&snapshot));
        if (!snapshot->HasKey("parent"))
            return std::string();
        const base::DictionaryValue* parent;
        EXPECT_TRUE(snapshot->GetDictionary("parent", &parent));
        std::string parent_id;
        EXPECT_TRUE(parent->GetString("id_ref", &parent_id));
        return parent_id;
    }

    std::string GetSnapshotURL(const trace_analyzer::TraceEvent* event)
    {
        const base::Value* arg_snapshot = event->arg_values.at("snapshot").get();
        const base::DictionaryValue* snapshot;
        EXPECT_TRUE(arg_snapshot->GetAsDictionary(&snapshot));
        if (!snapshot->HasKey("url"))
            return std::string();
        std::string url;
        EXPECT_TRUE(snapshot->GetString("url", &url));
        return url;
    }

} // namespace

class FrameTreeNodeBlameContextTest : public RenderViewHostImplTestHarness {
public:
    FrameTree* tree() { return contents()->GetFrameTree(); }
    FrameTreeNode* root() { return tree()->root(); }
    int process_id()
    {
        return root()->current_frame_host()->GetProcess()->GetID();
    }

    // Creates a frame tree specified by |shape|, which is a string of paired
    // parentheses. Each pair of parentheses represents a FrameTreeNode, and the
    // nesting of parentheses represents the parent-child relation between nodes.
    // Nodes represented by outer-most parentheses are children of the root node.
    // NOTE: Each node can have at most 9 child nodes, and the tree height (i.e.,
    // max # of edges in any root-to-leaf path) must be at most 9.
    // See the test cases for sample usage.
    void CreateFrameTree(const char* shape)
    {
        main_test_rfh()->InitializeRenderFrameIfNeeded();
        CreateSubframes(root(), 1, shape);
    }

    void RemoveAllNonRootFrames()
    {
        while (root()->child_count())
            tree()->RemoveFrame(root()->child_at(0));
    }

    void StartTracing()
    {
        base::trace_event::TraceLog::GetInstance()->SetEnabled(
            base::trace_event::TraceConfig("*"),
            base::trace_event::TraceLog::RECORDING_MODE);
    }

    void StopTracing()
    {
        base::trace_event::TraceLog::GetInstance()->SetDisabled();
    }

    std::unique_ptr<trace_analyzer::TraceAnalyzer> CreateTraceAnalyzer()
    {
        base::trace_event::TraceResultBuffer buffer;
        base::trace_event::TraceResultBuffer::SimpleOutput trace_output;
        buffer.SetOutputCallback(trace_output.GetCallback());
        base::RunLoop run_loop;
        buffer.Start();
        base::trace_event::TraceLog::GetInstance()->Flush(
            base::Bind(&OnTraceDataCollected, run_loop.QuitClosure(),
                base::Unretained(&buffer)));
        run_loop.Run();
        buffer.Finish();

        return base::WrapUnique(
            trace_analyzer::TraceAnalyzer::Create(trace_output.json_output));
    }

private:
    int CreateSubframes(FrameTreeNode* node, int self_id, const char* shape)
    {
        int consumption = 0;
        for (int child_num = 1; shape[consumption++] == '('; ++child_num) {
            int child_id = self_id * 10 + child_num;
            tree()->AddFrame(node, process_id(), child_id,
                blink::WebTreeScopeType::Document, std::string(),
                base::StringPrintf("uniqueName%d", child_id),
                blink::WebSandboxFlags::None, FrameOwnerProperties());
            FrameTreeNode* child = node->child_at(child_num - 1);
            consumption += CreateSubframes(child, child_id, shape + consumption);
        }
        return consumption;
    }
};

// Creates a frame tree, tests if (i) the creation of each new frame is
// correctly traced, and (ii) the topology given by the snapshots is correct.
TEST_F(FrameTreeNodeBlameContextTest, FrameCreation)
{
    /* Shape of the frame tree to be created:
   *        ()
   *      /    \
   *     ()    ()
   *    /  \   |
   *   ()  ()  ()
   *           |
   *           ()
   */
    const char* tree_shape = "(()())((()))";

    StartTracing();
    CreateFrameTree(tree_shape);
    StopTracing();

    std::unique_ptr<trace_analyzer::TraceAnalyzer> analyzer = CreateTraceAnalyzer();
    trace_analyzer::TraceEventVector events;
    trace_analyzer::Query q = trace_analyzer::Query::EventPhaseIs(TRACE_EVENT_PHASE_CREATE_OBJECT) || trace_analyzer::Query::EventPhaseIs(TRACE_EVENT_PHASE_SNAPSHOT_OBJECT);
    analyzer->FindEvents(q, &events);

    // Two events for each new node: creation and snapshot.
    EXPECT_EQ(12u, events.size());

    std::set<FrameTreeNode*> creation_traced;
    std::set<FrameTreeNode*> snapshot_traced;
    for (auto* event : events) {
        ExpectFrameTreeNodeObject(event);
        FrameTreeNode* node = tree()->FindByID(strtol(event->id.c_str(), nullptr, 16));
        EXPECT_NE(nullptr, node);
        if (event->HasArg("snapshot")) {
            ExpectFrameTreeNodeSnapshot(event);
            EXPECT_FALSE(base::ContainsValue(snapshot_traced, node));
            snapshot_traced.insert(node);
            std::string parent_id = GetParentNodeID(event);
            EXPECT_FALSE(parent_id.empty());
            EXPECT_EQ(node->parent(),
                tree()->FindByID(strtol(parent_id.c_str(), nullptr, 16)));
        } else {
            EXPECT_EQ(TRACE_EVENT_PHASE_CREATE_OBJECT, event->phase);
            EXPECT_FALSE(base::ContainsValue(creation_traced, node));
            creation_traced.insert(node);
        }
    }
}

// Deletes frames from a frame tree, tests if the destruction of each frame is
// correctly traced.
TEST_F(FrameTreeNodeBlameContextTest, FrameDeletion)
{
    /* Shape of the frame tree to be created:
   *        ()
   *      /    \
   *     ()    ()
   *    /  \   |
   *   ()  ()  ()
   *           |
   *           ()
   */
    const char* tree_shape = "(()())((()))";

    CreateFrameTree(tree_shape);
    std::set<int> node_ids;
    for (FrameTreeNode* node : tree()->Nodes())
        node_ids.insert(node->frame_tree_node_id());

    StartTracing();
    RemoveAllNonRootFrames();
    StopTracing();

    std::unique_ptr<trace_analyzer::TraceAnalyzer> analyzer = CreateTraceAnalyzer();
    trace_analyzer::TraceEventVector events;
    trace_analyzer::Query q = trace_analyzer::Query::EventPhaseIs(TRACE_EVENT_PHASE_DELETE_OBJECT);
    analyzer->FindEvents(q, &events);

    // The removal of all non-root nodes should be traced.
    EXPECT_EQ(6u, events.size());
    for (auto* event : events) {
        ExpectFrameTreeNodeObject(event);
        int id = strtol(event->id.c_str(), nullptr, 16);
        EXPECT_TRUE(base::ContainsValue(node_ids, id));
        node_ids.erase(id);
    }
}

// Changes URL of the root node. Tests if URL change is correctly traced.
TEST_F(FrameTreeNodeBlameContextTest, URLChange)
{
    main_test_rfh()->InitializeRenderFrameIfNeeded();
    GURL url1("http://a.com/");
    GURL url2("https://b.net/");

    StartTracing();
    root()->SetCurrentURL(url1);
    root()->SetCurrentURL(url2);
    root()->ResetForNewProcess();
    StopTracing();

    std::unique_ptr<trace_analyzer::TraceAnalyzer> analyzer = CreateTraceAnalyzer();
    trace_analyzer::TraceEventVector events;
    trace_analyzer::Query q = trace_analyzer::Query::EventPhaseIs(TRACE_EVENT_PHASE_SNAPSHOT_OBJECT);
    analyzer->FindEvents(q, &events);
    std::sort(events.begin(), events.end(), EventPointerCompare);

    // Three snapshots are traced, one for each URL change.
    EXPECT_EQ(3u, events.size());
    EXPECT_EQ(url1.spec(), GetSnapshotURL(events[0]));
    EXPECT_EQ(url2.spec(), GetSnapshotURL(events[1]));
    EXPECT_EQ("", GetSnapshotURL(events[2]));
}

} // namespace content
